﻿using System;
using System.Data;
using System.Reflection;
using System.Collections.Generic;
using System.Text;

namespace NetFramework.DataAccess.EntityBuilder
{
    public class EntityBuilder
    {
        private static readonly object bindingInfoSync = new object();

        private static Dictionary<string, EntityBindingInfo> bindingInfoCache = new Dictionary<string, EntityBindingInfo>();

        public static List<T> BuildEntityList<T>(DataTable table) where T : class, new()
        {
            List<T> list = new List<T>();

            foreach (DataRow row in table.Rows)
            {
                list.Add(BuildEntity<T>(row));
            }

            return list;
        }

        public static T BuildEntity<T>(IDataReader reader) where T : class, new()
        {
            T obj = default(T);

            if (reader.Read())
            {
                obj = BuildEntity<T>(new DataReaderDataSource(reader));
            }

            return obj;
        }

        public static List<T> ExecuteEntityList<T>(IDataReader reader) where T : class, new()
        {
            List<T> list = new List<T>();

            while (reader.Read())
            {
                list.Add(BuildEntity<T>(new DataReaderDataSource(reader)));
            }

            return list;
        }

        public static T BuildEntity<T>(DataRow row) where T : class, new()
        {
            return BuildEntity<T>(new DataRowDataSource(row));
        }

        private static T BuildEntity<T>(IDataSource dataSource) where T : class, new()
        {
            T obj = new T();

            FillEntity(typeof(T), obj, dataSource, string.Empty);

            return obj;
        }

        private static object BuildEntity(Type type, IDataSource dataSource, string prefix)
        {
            object obj = Activator.CreateInstance(type);

            FillEntity(type, obj, dataSource, prefix);

            return obj;
        }

        private static object FillEntity(Type type, object obj, IDataSource dataSource, string prefix)
        {
            EntityBindingInfo entityBindingInfo = HitBindingInfoCache(type);

            int columnCount = dataSource.ColumnCount;
            for (int i = 0; i < columnCount; i++)
            {
                BindingInfo bindingInfo = entityBindingInfo.HitBindingInfo(prefix, dataSource.GetName(i));
                if (bindingInfo != null)
                {
                    object setValue = dataSource[i];
                    if(setValue != DBNull.Value)
                    {
                        bindingInfo.SetValue(obj, setValue, dataSource.GetColumnType(i));
                    }
                }
            }

            if (entityBindingInfo.ReferenceBindingInfoList != null &&
                entityBindingInfo.ReferenceBindingInfoList.Count > 0)
            {
                foreach (ReferenceBindingInfo refBinding in entityBindingInfo.ReferenceBindingInfoList)
                {
                    refBinding.SetValue(obj, BuildEntity(refBinding.Type, dataSource, refBinding.PrefixName));
                }
            }

            return obj;
        }

        private static EntityBindingInfo HitBindingInfoCache(Type type)
        {
            EntityBindingInfo info;
            if (!bindingInfoCache.TryGetValue(type.FullName, out info))
            {
                lock (bindingInfoSync)
                {
                    if (!bindingInfoCache.TryGetValue(type.FullName, out info))
                    {
                        info = GetDataBindingList(type);
                        bindingInfoCache.Add(type.FullName, info);
                    }
                }
            }

            return info;
        }

        private static EntityBindingInfo GetDataBindingList(Type type)
        {
            EntityBindingInfo entity = new EntityBindingInfo();

            List<BindingInfo> bindingList = new List<BindingInfo>();
            List<ReferenceBindingInfo> referenceBindingList = new List<ReferenceBindingInfo>();

            PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (PropertyInfo property in properties)
            {
                object[] customerAttributes = property.GetCustomAttributes(false);
                if (customerAttributes != null)
                {
                    foreach (object attr in customerAttributes)
                    {
                        if (attr is DataMappingAttribute)
                        {
                            bindingList.Add(new BindingInfo(((DataMappingAttribute)attr).ColumnName, property));
                            break;
                        }
                        else if (attr is ReferencedEntityAttribute)
                        {
                            referenceBindingList.Add(new ReferenceBindingInfo(((ReferencedEntityAttribute)attr).Prefix, property));
                            break;
                        }
                    }
                }
            }

            entity.BindingInfoList = bindingList;
            entity.ReferenceBindingInfoList = referenceBindingList;

            return entity;
        }
    }
}
